using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using FoundationLib.Ifc.Data;
using FoundationLib.Ifc.Domain;
using System;
using System.Runtime.Caching;


namespace FoundationLib.Ifc.Data 
{
	/// <summary>
	/// Provides an abstract base class for event-sourced aggregate root repositories
	/// </summary>
	public abstract class EventSourcedAggregateRepository<TAggregate> : IRepository<TAggregate>
		where TAggregate : class, IEventSourcedAggregate 
	{
		/// <summary>
		/// The event storage associated with this repository
		/// </summary>
		private IAggregateEventStorage storage;
		/// <summary>
		/// The repository configuration
		/// </summary>
		private RepositoryConfiguration configuration;
		/// <summary>
		/// The underlying message bus
		/// </summary>
		private IMessageBus bus;
		/// <summary>
		/// The underlying object cache
		/// </summary>
		private IDictionary<object, IEventSourcedAggregate> cache;
		/// <summary>
		/// Static lock object
		/// </summary>
		private static readonly object s_lock = new Object();

		/// <summary>
		/// Creates a new event-sourced aggregate repository
		/// </summary>
		/// <param name="cache">The underlying object cache to use</param>
		/// <param name="storage">The underlying event storage</param>
		/// <param name="bus">The message bus used  to publish domain events</param>
		public EventSourcedAggregateRepository(IDictionary<object, IEventSourcedAggregate> cache, 
			IAggregateEventStorage storage, IMessageBus bus)
		{
			if (cache == null)
				throw new ArgumentNullException("cache");
			if (storage == null)
				throw new ArgumentNullException("storage");
			if (bus == null)
				throw new ArgumentNullException("bus");

			this.cache = cache;
			this.storage = storage;
			this.bus = bus;
			this.configuration = new RepositoryConfiguration();
		}

		/// <summary>
		/// Adds the given entity to the repository
		/// </summary>
		/// <param name="instance">The entity to be added</param>
		public virtual void Add(TAggregate instance)
		{
			if (instance == null)
				throw new ArgumentNullException("instance");
			
			lock(s_lock)
			{
				object key = this.GetAggregateKey(instance);
				this.cache[key] = instance;
			}
			
		}

		/// <summary>
		/// Retrieves the unique key for the given aggregate
		/// </summary>
		/// <param name="instance">The aggregate</param>
		/// <returns>The unique aggregate key</returns>
		protected abstract object GetAggregateKey(TAggregate instance);

		/// <summary>
		/// Configures this repository instance using a fluent interface
		/// </summary>
		public RepositoryConfiguration Configure()
		{
			return this.configuration;
		}

		/// <summary>
		/// Configures this repository instance
		/// </summary>
		/// <param name="config">The configuration</param>
		public void Configure(RepositoryConfiguration config)
		{
			if (config == null)
				throw new ArgumentNullException("config");
			this.configuration = config;
		}

		/// <summary>
		/// Creates a memento (serialization) of the given domain event
		/// </summary>
		/// <returns>The domain event memento</returns>
		/// <param name="domainEvent">The domain event</param>
		protected abstract object CreateEventData(IVersionedEvent domainEvent);

		/// <summary>
		/// Creates a new aggregate instance
		/// </summary>
		/// <returns>The instance created</returns>
		protected abstract TAggregate CreateInstance();

		/// <summary>
		/// Creates an aggregate from the given snapshot
		/// </summary>
		/// <param name="snapshot">The snapshot data</param>
		/// <returns>The aggregate created</returns>
		protected abstract TAggregate CreateInstance(object snapshot);

		/// <summary>
		/// Creates a snapshot (serialization) of the given aggregate instance
		/// </summary>
		/// <returns>The serialized aggregate data</returns>
		/// <param name="aggregate">The aggregate</param>
		protected abstract object CreateSnapshot(IAggregate aggregate);

		/// <summary>
		/// Gets the entity with the given unique identifier from the repository
		/// </summary>
		/// <returns>The entity or null if none was found for the given key</returns>
		/// <param name="key">the unique entity key</param>
		public TAggregate Get(object key)
		{
			lock (s_lock)
			{
				return (this.cache[key.ToString()] as TAggregate ?? this.Load(key));
			}
		}

		/// <summary>
		/// Loads the aggregate from the underlying event storage
		/// </summary>
		/// <param name="key">The aggregate key</param>
		/// <returns>The aggregate</returns>
		protected virtual TAggregate Load(object key)
		{
			TAggregate result = null;
			IEnumerable<IVersionedEvent> eventsToApply;
			ISnapshotAggregateEventStorage snapshotStorage = this.storage as ISnapshotAggregateEventStorage;
			
			if (snapshotStorage != null)
			{
				//If snapshotting is supported, we get the latest snapshot and only events occurring after that snapshot
				object snapshot = snapshotStorage.GetLatestSnapshot(key);
				eventsToApply = snapshotStorage.GetAllSinceLatestSnapshot(key);
				if (snapshot != null)
				{
					result = this.CreateInstance(snapshot);
				}
			}
			else
			{
				//Otherwise, we get all events ever stored for the aggregate
				eventsToApply = this.storage.GetAll(key);
				result = this.CreateInstance(key);
				this.cache.Add(key, result);
			}

			if (result != null && eventsToApply != null)
			{
				//Apply events from history and add the aggregate to the underlying cache
				result.LoadFromHistory(eventsToApply);
				this.cache.Add(key, result);
			}

			return result as TAggregate;
		}

		/// <summary>
		/// Publishes all uncommitted changes
		/// </summary>
		/// <param name="changes">All pending changes to be published</param>
		protected virtual void PublishEvents(IPendingChanges changes)
		{
			if (changes == null)
				throw new ArgumentNullException("changes");
			foreach (var item in changes)
			{
				this.bus.Publish(item);
			}
		}

		/// <summary>
		/// Removes the given entity from the repository
		/// </summary>
		/// <remarks>Removing an event-sourced aggregate from the underlying event storage is not possible. 
		/// Instead, this method will simply remove (detach) the aggregate from the cache.</remarks>
		/// <returns>True if the entity has been removed, false otherwise</returns>
		/// <param name="entity">The entity to remove from the repository</param>
		public virtual bool Remove(TAggregate entity)
		{
			return this.cache.Remove(this.GetAggregateKey(entity));
		}


		/// <summary>
		/// Submits all changes in the repository to the underlying data store
		/// </summary>
		public virtual void SubmitChanges()
		{
			//Determine if snapshotting is possible and enabled
			ISnapshotAggregateEventStorage snapshotStorage = this.storage as ISnapshotAggregateEventStorage;
			bool createSnapshot = (this.configuration.EnableSnapshotting == true && snapshotStorage != null);
			int threshold = this.configuration.SnapshotEventThreshold;
			
			lock (s_lock)
			{
				//We're only interested in aggregates with pending changes
				foreach (TAggregate aggregate in this.cache.Values.Where(x => x.Changes.Any()))
				{
					//First, get the key and save all pending aggregate events to the underlying event storage
					object key = this.GetAggregateKey(aggregate);
					this.storage.Save(key, aggregate.Changes);

					//Now, if snapshotting is enabled, evaluate if the snapshotting threshold is reached
					//The snaphshot might occur after more events than defined by the threshold
					if (createSnapshot && aggregate.Changes.Select(x => x.Version).Any(x => (x % threshold) == 0))
					{
						//Create a snapshot of the LATEST (i.e. current) version of the aggregate and save it
						object snapshot = this.CreateSnapshot(aggregate);
						if (snapshot != null)
							snapshotStorage.SaveSnapshot(key, aggregate.Version, snapshot);
					}

					//All pending changes are now published to the underlying bus and then cleared
					this.PublishEvents(aggregate.Changes);
					aggregate.Changes.Clear();
				}
			}
		}

	}

}